Skip to content

fix(release): consolidate release locks, prerelease logic and version truth (#1231) - #1290

Merged
SisyphusZheng merged 1 commit into
devfrom
fix/1231-release-lock-version-truth
Sep 3, 2026
Merged

fix(release): consolidate release locks, prerelease logic and version truth (#1231)#1290
SisyphusZheng merged 1 commit into
devfrom
fix/1231-release-lock-version-truth

Conversation

@SisyphusZheng

Copy link
Copy Markdown
Member

Closes #1231. Part of umbrella #1155; stage issue #1288. Conforms to the #1216 canonical verdict contract (tools/gate-verdict.ts) — this PR touches no gate verdicts, only version truth, locking and whitelist semantics.

Problem

  • M14: CI serializes releases via the openelement-release concurrency group in .github/workflows/autoflow-release.yml, but nothing stopped two local release operations from interleaving in one working copy (both rewriting project-constants.ts, evidence records and manifests mid-plan).
  • M16: prerelease/version magic was re-rolled in ≥6 production modules (parse regexes in project-constants.ts, autoflow/version-anchors.ts, channel maps in npm-release-verifier.ts/publish-npm.ts/check-release-truth.ts, alpha-number extraction in three docs gates, normalizeReleaseVersion in autoflow/cli.ts, semver parse/compare in bump-version.ts and autoflow/release.ts); 0.41.0-alpha.14 (the immutable-tag policy boundary) was hard-coded twice.
  • M17: the docs-truth current-gate whitelist matched by substring (file.includes(entry)), exempting any path containing tokens like migration/archive anywhere (e.g. docs/runbooks/supabase-migrations.md slipped through via migration).
  • freeze:semantics (Beta.1 carried risk): local and CI runs of freeze:semantics:check compute different diffs (local base origin/main vs CI base origin/$GITHUB_BASE_REF) and CI additionally reads the PR body — an uncharacterized local/CI semantics gap.

Owner

Release tooling / governance gates (tools/, tools/autoflow/) — single owner: the release-toolchain surface already owning autoflow/release.ts, publish-npm.ts and the docs-truth gates.

Before

  • No local release lock; concurrent local releases could interleave writes.
  • 10 production copies of prerelease parse/compare/channel logic; FIRST_TAGGED_VERSION duplicated in autoflow/release.ts:533 and check-docs-truth.ts:740.
  • Whitelist substring matching; dead entries (legacy, sop/, docs/arch/, …) matched nothing while lookalike paths were silently exempt.
  • freeze:semantics local/CI divergence undocumented and unproven.

After

  • M14: tools/autoflow/release-lock.ts — deterministic lock at .artifacts/autoflow-release.lock (gitignored scratch) acquired atomically (createNew) by every release-mutating CLI command (patch-release, release, release-prepare, publish-existing, release-record). A second local release fails closed, naming the holder (pid/command/startedAt). Released via finally (exception paths) and an unload hook (Deno.exit paths, verified to fire); a hard kill leaves a stale lock that the next run reports by name — recovery is a deliberate human act, never silent lock-breaking. CI exclusion remains the workflow concurrency group.
  • M16: one canonical module tools/lib/version.tsparseLineVersion/tryParseLineVersion (strict x.y.z(-label.n), no @std/semver: the module is import-free so project-constants.ts stays loadable by Nitro/jiti under Node), prereleaseParts, prereleaseSequence, prereleaseChannel (the alpha|beta|rc dist-tag set), compareVersions, nextPatchVersion, normalizeReleaseVersion, FIRST_TAGGED_VERSION. All ten former copies now import it; established export names (parseVersion, compareVersions, nextPatchVersion, normalizeReleaseVersion) remain as thin delegates so existing call sites/tests are unchanged.
  • M17: CURRENT_DOC_ALLOWED is exact-path: trailing-/ entries are repo-anchored directory prefixes, other entries are exact file paths; matching exported as isCurrentDocAllowed(). Dead entries dropped; the one incidentally-exempt file (docs/runbooks/supabase-migrations.md) is now gated and passes the scan.
  • freeze:semantics: gap closed by proof + documentation in the tools/check-frozen-semantics.ts header. Divergence is one-directional (fail closed): under the ADR-0151 train topology origin/main is an ancestor of every dev-based PR HEAD (verified: git merge-base --is-ancestor origin/main origin/dev ✓, 0 commits on main not in dev), so the local diff is a superset of the CI diff while local amendment signals are a subset (no PR body) — evaluate(local)=ok ⇒ evaluate(CI)=ok; a local green can never mask a red CI. The residual gap is only local false-FAIL (dev drift in the wider diff, PR-body-only citations). The header also records why defaulting the local base to origin/dev would be fail-open for dev→main release PRs (empty local diff vs main-diff in CI).

Why-not-second-owner

All four items live in the same release-toolchain/docs-gate surface and interlock through the one new module (tools/lib/version.ts); splitting M16 from M14/M17 would create cross-PR import conflicts in autoflow/cli.ts and check-docs-truth.ts without changing ownership. No product package, public API or frozen-semantics path is touched (no ADR-0151 citation required; freeze:semantics:check passes).

Evidence

  • RED→GREEN: new tests failed before implementation (missing tools/lib/version.ts, release-lock.ts, isCurrentDocAllowed); green after.
  • deno task test → exit 0 (full suite incl. tools/ and the supabase starter).
  • deno fmt --check / deno lint → clean.
  • deno task docs:truth → all 7 docs-truth gates + release-truth + version anchors (25 anchors) + recipe parity pass.
  • deno task freeze:semantics:check → exit 0. graph:check, arch:check, type-safety:check, package-surface:check, interface:snapshot, repo:hygiene, release:state-machine:check → pass.
  • Publishability: deno task pack:dry-run → 5 tarballs; deno task package-artifacts:check → pass. No relative cross-package imports added (tools-only change).
  • Lock end-to-end (local, real CLI): exception path releases the lock; a held lock makes publish-existing exit 1 with the holder named; the holder's lock file is left untouched.
  • CI gating: M17 is enforced by the existing docs:check-current/docs:truth CI gates; M14 is machinery (not a qualification) — CI mutual exclusion was and remains the workflow concurrency group, so no new CI wiring is applicable; the freeze-gap closure is a documented proof, no new gate.

Scope

In: tools/lib/version.ts (+test), tools/autoflow/release-lock.ts (+test), tools/autoflow/{cli,release,version-anchors}.ts, tools/{bump-version,project-constants,check-strategic-docs,check-version-anchors,check-docs-truth,check-release-truth,check-frozen-semantics,publish-npm}.ts, tools/lib/{npm-release-verifier,release-evidence-consistency}.ts, tools/check-docs-truth.test.ts. Out: no frozen-semantics product paths, no package/runtime changes, no new governance machinery beyond the M14 lock the issue mandates, no CI workflow changes (existing gates cover the changed checks).

Risk

  • The canonical parser is stricter than the old @std/semver-based copies on exotic inputs (build metadata, multi-part prereleases, leading zeros) — all outside the documented strict x.y.z(-label.n) line contract and now consistently rejected everywhere; all pre-existing behavior tests pass unchanged.
  • A hard-killed local release leaves a stale lock requiring manual removal (by design; the error names the exact file).
  • M17 newly gates previously-exempt lookalike paths; the full docs:truth suite passes, so no current content relies on substring exemptions.

… truth (#1231)

M14: add a deterministic local release lock (tools/autoflow/release-lock.ts)
taken by every release-mutating autoflow command (patch-release, release,
release-prepare, publish-existing, release-record). Acquisition is atomic
(createNew) under the gitignored .artifacts/ scratch dir; a second local
release operation fails closed and names the holder, and the lock is released
on both the Deno.exit and exception paths. CI mutual exclusion keeps using
the openelement-release concurrency group in autoflow-release.yml.

M16: one canonical prerelease/version truth implementation in
tools/lib/version.ts (import-free, so project-constants.ts stays loadable by
Nitro/jiti under Node). bump-version, check-strategic-docs,
check-version-anchors, check-docs-truth, check-release-truth,
npm-release-verifier, release-evidence-consistency, autoflow cli/release/
version-anchors and publish-npm now import parseLineVersion /
prereleaseParts / prereleaseSequence / prereleaseChannel / compareVersions /
nextPatchVersion / normalizeReleaseVersion from it instead of re-rolling
regexes; FIRST_TAGGED_VERSION ('0.41.0-alpha.14') now has a single copy
(previously hard-coded in both autoflow/release.ts and check-docs-truth.ts).

M17: the docs-truth current gate whitelist (CURRENT_DOC_ALLOWED) switches
from substring matching to exact-path semantics: trailing-'/' entries are
repo-anchored directory prefixes, all other entries are exact file paths.
Incidental substring exemptions (e.g. docs/runbooks/supabase-migrations.md
via 'migration') are now gated and pass the scan; dead entries dropped.

freeze:semantics local/CI diff-semantics gap (Beta.1 carried risk): the
module header now carries the precise characterization — divergence between
the origin/main local base and the origin/$GITHUB_BASE_REF CI base is
provably one-directional (fail closed) under the ADR-0151 train topology
(origin/main is an ancestor of every dev-based PR HEAD, so the local diff is
a superset of the CI diff while local amendment signals are a subset), and
documents why defaulting the local base to origin/dev would be fail-open for
dev→main release PRs.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

APIError: Insufficient Balance

opencode session  |  github run

@SisyphusZheng
SisyphusZheng merged commit ac909e0 into dev Sep 3, 2026
10 checks passed
@SisyphusZheng
SisyphusZheng deleted the fix/1231-release-lock-version-truth branch September 3, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant